<?php
declare (strict_types=1);

namespace iot;

use aliyun\AliyunOss;
use app\admin\model\base\SysConfig;
use app\admin\model\base\User;
use app\admin\model\iot\Device;
use app\admin\model\iot\DeviceApkLog;
use app\admin\model\iot\DeviceLog;
use app\admin\model\iot\DeviceLogError;
use app\admin\model\iot\ScanBagDay;
use app\admin\model\iot\ScanBagOrder;
use app\BaseService;
use app\datacenter\model\Machine;
use emqx\EmqxHttpApi;
use job\ThinkQueueJob;
use redis\RedisTools2;
use think\facade\Cache;
use think\facade\Cookie;
use think\facade\Log;

/**
 * 设备客户端服务层
 */
class DeviceClient extends BaseService
{
    //保存实例的对象
    private static $_instance;
    //处理数据属性
    private mixed $msg_arr;
    private array $device_params = [];
    private mixed $DeviceModel;
    //公共参数
    public int $CMD;
    public string $imei;
    public string $ReqId;

    /**
     * 定义构造方法
     */
    private function __construct()
    {
        //加载系统配置
        SysConfig::loadConfig();
    }

    /**
     * 定义一个空方法防止被外部克隆
     * @return void
     */
    private function __clone()
    {
    }

    /**
     * 定义一个获取实例对象的方法
     * @return DeviceClient
     */
    public static function getInstance(): DeviceClient
    {
        if (!(self::$_instance instanceof DeviceClient)) {
            self::$_instance = new self();
        }
        //返回实例对象
        return self::$_instance;
    }

    /**
     * MQTT节点上线系统消息
     * @param array $data
     * @return void
     */
    public function sysMsgConnected(array $data)
    {
        $msg = json_decode($data['msg'], true);
        $imei = $msg['clientid'];
        DeviceLog::writeDeviceLog(0, $imei, uuid(), $data['msg'], '上行', '上线', $data['time'], '设备上线');
        //更新设备表
        Device::update(['online' => 1, 'online_time' => $data['time'], 'last_time' => $data['time']], ['imei' => $imei]);
    }

    /**
     * MQTT节点离线线系统消息
     * @param array $data
     * @return void
     */
    public function sysMsgDisconnected(array $data)
    {
        $msg = json_decode($data['msg'], true);
        $imei = $msg['clientid'];
        DeviceLog::writeDeviceLog(0, $imei, uuid(), $data['msg'], '上行', '下线', $data['time'], '设备下线');
        //更新设备表
        Device::update(['online' => 0, 'offline_time' => $data['time']], ['imei' => $imei]);
    }

    /**
     * 客户端设备上报
     * @param string $msg
     * @param int $msg_time
     * @return bool
     */
    public function clientMessage(string $msg, int $msg_time): bool
    {
        //解析数据
        $this->msg_arr = json_decode($msg, true);
        if (!$this->msg_arr or !isset($this->msg_arr['CMD']) or !isset($this->msg_arr['IMEI']) or !isset($this->msg_arr['REQ'])) {
            DeviceLog::writeDeviceLog(0, '', uuid(), $msg, '上行', '失败', $msg_time, '接收成功');
            return false;
        }
        $this->CMD = $this->msg_arr['CMD'];
        $this->imei = $this->msg_arr['IMEI'];
        $this->ReqId = $this->msg_arr['REQ'];
        //写入日志
        DeviceLog::writeDeviceLog($this->CMD, $this->imei, $this->ReqId, $msg, '上行', '成功', $msg_time, '接收成功');
        //获取设备表模型
        $this->DeviceModel = Device::where(['imei' => $this->imei])->find();
        if (!$this->DeviceModel) {
            return false;
        }
        //处理数据
        $this->handleUpData();
        //更新上报时间到V1
        Machine::update(['last_online_time'=>$msg_time],['mac_id'=>$this->DeviceModel->mac_id]);
        //保存至设备表
        $this->device_params['online'] = 1;
        $this->device_params['last_time'] = $msg_time;
        $this->DeviceModel->save($this->device_params);
        return true;
    }

    /**
     * 处理上报数据
     * @return void
     */
    private function handleUpData(): void
    {
        //处理事件
        switch ($this->CMD) {
            case 41:
                //订单号
                $ON = $this->msg_arr['ON'];
                $OS = $this->msg_arr['OS'];
                //出货状态
                $State = [0 => '出袋成功', 2 => '卡袋', 3 => '无袋子', 8 => '设备故障', 9 => '出货装置无响应',];
                $order_state = $State[$OS] ?? '出袋失败';
                //删除设备正在使用中缓存
                $tKeyImei = BaseService::generateBagOutProgramKey($this->imei);
                $tKeyImei_work = $tKeyImei.'working';
                if (Cache::has($tKeyImei)) {
                    Cache::delete($tKeyImei);
                }
                $redis = RedisTools2::getInstance(config('cache.stores.redis'));
                if ($redis->exists($tKeyImei_work)) {
                    $redis->del($tKeyImei_work);
                }

                //推送出袋结果
                $emqx = new EmqxHttpApi();
                $topic_js = $emqx->emqx_scan_bag_topic_js . $ON;
                SetCookie('run_strategy','0',30,"/");
                $emqx->publish($topic_js, ['status' => $OS == 0 ? 3 : 4, 'wait_msg' => $order_state]);
                //获取出袋订单对象
                $order_info = ScanBagOrder::findOne(['order_no' => $ON]);
                if ($order_info) {
                    //更新出货结果记录
                    ScanBagOrder::update(['order_state' => $order_state], ['order_no' => $ON]);
                    //出袋成功
                    if ($OS == 0) {
                        //减少余量
                        $allowance = $this->DeviceModel->allowance;
                        $this->device_params['allowance'] = match ($allowance) {
                            0 => 499,
                            default => $allowance - 1,
                        };
                        //写入当天出袋记录
                        ScanBagDay::writeUserIdDayNum($order_info['user_id']);
                    }
                    //同步订单至V1分润系统写入队列
                    $syn_data = ['user_id' => $order_info['user_id'], 'order_no' => $ON];
                    ThinkQueueJob::sendMsg('SynOrderV2ToV1', '', $syn_data);
                }
                //处理设备故障
                if (in_array($OS, [2, 3, 8, 9])) {
                    $this->eventHand($this->imei, $OS);
                }
                break;
            case 57:
                $AR = $this->msg_arr['AR'];
                //升级结果
                $State = [0 => '开始下载', 1 => '下载失败', 2 => '开始安装', 3 => '升级成功', 4 => '升级失败',];
                $ApkRes = $State[$AR] ?? '升级失败';
                //更新数据库
                DeviceApkLog::updateApkLog($this->ReqId, $ApkRes);
                //处理设备故障
                if (in_array($AR, [1, 4])) {
                    $this->eventHand($this->imei, 1);
                }
                break;
            default:
                break;
        }
        //处理其他事件属性值
        foreach ($this->msg_arr as $k => $v) {
            switch ($k) {
                case 'MID'://后端注册返回的mac_id
                    //$this->device_params['mac_id'] = strval($v);
                    break;
                case 'SPS'://串口通讯状态 (0通讯不上，1通讯上)
                    $this->device_params['sp_state'] = $v ? 1 : 0;
                    //处理设备故障
                    if ($v == 0) {
                        $this->eventHand($this->imei, 5);
                    }
                    break;
                case 'HBD'://人体检测装置模块 (0通讯不上，1通讯上)
                    $this->device_params['hbd'] = $v ? 1 : 0;
                    break;
                case 'HBDN'://人体检测次数
                    $this->device_params['hbd_num'] = intval($v);
                    break;
                case 'NET'://网络信息状态强弱 0~5,5是最强
                    $this->device_params['network'] = intval($v);
                    break;
                case 'TIT'://定时开关机类型 1是每天  2指定日期范围
                    $this->device_params['time_type'] = intval($v);
                    break;
                case 'OFT'://定时关机时间 格式：2022-02-18 18:39:00
                    $this->device_params['off_time'] = $v;
                    break;
                case 'ONT'://定时开机时间 格式：2022-02-18 18:39:00
                    $this->device_params['on_time'] = $v;
                    break;
                case 'MT'://电机出货时间设置9~99
                    $this->device_params['motor'] = intval($v);
                    break;
                case 'VL'://当前音量
                    $this->device_params['volume'] = intval($v);
                    break;
                case 'LC'://设备上传日志 1开启 ，0关闭
                    $this->device_params['log_cat'] = $v ? 1 : 0;
                    break;
                case 'LM'://设备日志详情
                    break;
                case 'AWS'://袋子余量传感器开关
                    //传感器问题暂不处理
                    break;
                case 'OA'://货道号 默认为1
                    break;
                case 'AV'://设备Apk版本号
                    $this->device_params['apk'] = $v;
                    break;
                case 'AT'://设备类型
                    $this->device_params['type'] = intval($v);
                    break;
                default:
                    break;
            }
        }
    }

    /**
     * 同步订单至V1分润系统
     * @param array $syn_data
     * @return void
     */
    public function synOrderV2ToV1(array $syn_data)
    {
        $user_id = $syn_data['user_id'];
        $order_no = $syn_data['order_no'];
        try {
            //获取用户信息
            $user_info = User::getIdUserInfo($user_id);
            //获取订单信息
            $order_info = ScanBagOrder::findOne(['order_no' => $order_no]);
            //对应V1系统支付状态枚举
            $pay_pattern = $order_info['pay_pattern'] == '免费' ? 0 : 1;
            $pay_type = $order_info['pay_type'] == '免费' ? 1 : 4;
            $status = $order_info['order_state'] == '出袋成功' ? 1 : 2;
            //阿里云默认头像
            $AliyunOss = new AliyunOss();
            //组装请求数据
            $body_data = [
                'openid' => $user_info['openid'],
                'avatar' => $user_info['avatar'] ?: $AliyunOss->getObjectUrl(config('ws_config.avatar')),
                'nickname' => trim($user_info['nickname']) ?: '未知',
                'mac_id' => $order_info['mac_id'],
                'pay_pattern' => $pay_pattern,
                'pay_type' => $pay_type,
                'order_sn' => $order_info['order_no'],
                'status' => $status
            ];
            //计算签名
            $sign = arrayDataSignature($body_data);
            $body_data['sign'] = $sign;
            $params_ready = [
                'form_params' => $body_data
            ];
            //请求V1系统接口
            $bind_machine_url = config('sys_config.WsScanBag.v1_iot_pay_uri') . '/api/machine/addCostOrder';
            $result = httpPost($bind_machine_url, $params_ready);
            $content = json_decode($result['content'], true);
            if ($result['status_code'] == 200 && $content['code'] == 200) {
                return;
            }
        } catch (\Exception $exception) {
            Log::write($exception->getMessage(), '同步订单至V1分润系统');
        }
        //未同步成功则写入记录表
        ThinkQueueJob::sendMsg('SynOrderV2ToV1', '', $syn_data, 300);
    }

    ############################################# 设备事件处理 ################################################

    /**
     * 设备事件处理
     * @param string $imei
     * @param int $event
     * @return bool
     */
    public function eventHand(string $imei, int $event): bool
    {
        switch ($event) {
            case 1:
                $content = '升级失败';
                break;
            case 2:
                $content = '卡袋';
                break;
            case 3:
                Device::deviceAllowance($imei);
                $content = '无袋子';
                //执行短信通知
                break;
            case 4:
                $content = 'MQTT连接不上';
                break;
            case 5:
                $content = '串口通讯不上';
                break;
            case 6:
                $content = '余量传感器异常';
                break;
            case 7:
                $content = '网络信号弱';
                break;
            case 8:
                $content = '设备故障';
                break;
            case 9:
                $content = '出货装置无响应';
                break;
            default:
                return false;
        }
        //写入设备错误日志
        DeviceLogError::writeDeviceErrorLog($imei, $event, $content);
        return true;
    }

    ############################################# 设备操作指令 ###################################################

    /**
     * 处理订单排队队列
     * @param array $msg
     * @return bool
     */
    public function processOrderQueue(array $msg): bool
    {
        $this_time = time();
        $emqx = new EmqxHttpApi();
        $tKeyMacid = BaseService::generateScanBagQueueKey($msg['mac_id']);
        $tKeyImei = BaseService::generateBagOutProgramKey($msg['imei']);
        //获取出袋队列
        $redis = RedisTools2::getInstance(config('cache.stores.redis'));
        $dataAll = $redis->lrange($tKeyMacid, 0, -1);
        //获取缓存

        //判断有无正在使用中的任务
        $mac_use = $redis->setnx($tKeyImei.'working',1);
        $cache_value = Cache::get($tKeyImei);
        if (!$mac_use) {//正在使用
            //存在排队则写回触发队列
            ThinkQueueJob::sendMsg('ProcessOrderQueue', '', $msg, 2);
            //判断当前这条是否超时,如果超时则出货失败
            if($cache_value){
                if ($cache_value['cmd_time'] < ($this_time - 20)) {
                    //已超时,删除缓存
                    Cache::delete($tKeyImei);
                    //推送至前端告诉正在出袋
                    $topic_js = $emqx->emqx_scan_bag_topic_js . $cache_value['order_no'];
                    $emqx->publish($topic_js, ['status' => 4, 'wait_msg' => BaseService::$EnumQueueStatus[4]]);
                }
            }
            //未超时不处理
            return true;
        } else {
            $redis->expire($tKeyImei.'working',180);
            //未使用,则下发指令出货
            $data_one = $redis->lPop($tKeyMacid);
            if (!$data_one) return false;
//            //存在排队则写回触发队列
//            ThinkQueueJob::sendMsg('ProcessOrderQueue', '', $msg, 2);
            //解码下发出货指令
            $data_one = json_decode($data_one, true);
            $this->shippingOrder($data_one['imei'], $data_one['order_no']);
            //写入机器出袋缓存
            $cache_value = ['order_no' => $data_one['order_no'], 'cmd_time' => $this_time];
            Cache::set($tKeyImei, $cache_value);
            //推送至前端告诉正在出袋
            $topic_js = $emqx->emqx_scan_bag_topic_js . $cache_value['order_no'];
            $emqx->publish($topic_js, ['status' => 2, 'wait_msg' => BaseService::$EnumQueueStatus[2]]);
            unset($dataAll[0]);
        }
        //推送至前端
        foreach ($dataAll as $rank => $value) {
            $value = json_decode($value, true);
            //推送至前端
            $temp2['status'] = 1;
            $temp2['wait_msg'] = BaseService::$EnumQueueStatus[1];
            $temp2['wait_num'] = $rank;
            $temp2['wait_total'] = count($dataAll);
            $topic_js = $emqx->emqx_scan_bag_topic_js . $value['order_no'];
            $emqx->publish($topic_js, $temp2);
        }
        return true;
    }

    /**
     * 出货事件
     * @param string $imei
     * @param string $OrderNo
     * @return bool
     */
    public function shippingOrder(string $imei, string $OrderNo): bool
    {
        $data = [
            'OA' => 1,
            'ON' => $OrderNo,
        ];
        return self::commandDown(41, $imei, $data, '出货事件');
    }

    /**
     * 电机出货长度事件
     * @param string $imei
     * @param int $motor
     * @return bool
     */
    public function motorDeliveryLength(string $imei, int $motor): bool
    {
        if ($motor < 1 or $motor > 99) {
            return false;
        }
        $data = ['MT' => $motor];
        return self::commandDown(43, $imei, $data, '电机出货长度事件');
    }

    /**
     * 袋子余量传感器
     * @param string $imei
     * @return bool
     */
    public function allowanceQuery(string $imei): bool
    {
        $data = [];
        return self::commandDown(45, $imei, $data, '袋子余量传感器');
    }

    /**
     * 定时开关机事件
     * @param string $imei
     * @param int $TimeType
     * @param string $OnTime
     * @param string $OffTime
     * @return bool
     */
    public function timingSwitch(string $imei, int $TimeType, string $OnTime, string $OffTime): bool
    {
        $data = [
            'TIT' => $TimeType,
            'ONT' => $OnTime,
            'OFT' => $OffTime,
        ];
        return self::commandDown(51, $imei, $data, '定时开关机事件');
    }

    /**
     * 音量调整事件
     * @param string $imei
     * @param int $Volume
     * @return bool
     */
    public function volumeSet(string $imei, int $Volume): bool
    {
        if ($Volume < 0 or $Volume > 100) {
            return false;
        }
        $data = ['VL' => $Volume,];
        return self::commandDown(52, $imei, $data, '音量调整事件');
    }

    /**
     * 设备重启事件
     * @param string $imei
     * @return bool
     */
    public function deviceRestart(string $imei): bool
    {
        $data = [];
        return self::commandDown(53, $imei, $data, '设备重启事件');
    }

    /**
     * 日志分析指令
     * @param string $imei
     * @param int $Logcat
     * @return bool
     */
    public function deviceLogCat(string $imei, int $Logcat): bool
    {
        //验证设备类型参数
        if (!in_array($Logcat, [0, 1])) {
            return false;
        }
        $data = ['LC' => $Logcat,];
        return self::commandDown(59, $imei, $data, '日志分析指令');
    }

    /**
     * APK升级通知事件
     * @param string $imei
     * @param string $ReqId
     * @param int $ApkType
     * @param string $ApkVer
     * @param string $ApkUrl
     * @return bool
     */
    public function apkUpgrade(string $imei, string $ReqId, int $ApkType, string $ApkVer, string $ApkUrl): bool
    {
        //验证设备类型参数
        if (!array_key_exists($ApkType, Device::$deviceTypeArr)) {
            return false;
        }
        $data = [
            'REQ' => $ReqId,
            'AT' => $ApkType,
            'AV' => $ApkVer,
            'AU' => $ApkUrl,
        ];
        return self::commandDown(57, $imei, $data, 'APK升级通知事件');
    }

    /**
     * 统一下发指令
     * @param int $Cmd
     * @param string $Imei
     * @param array $data
     * @param string $remark
     * @return bool
     */
    public static function commandDown(int $Cmd, string $Imei, array $data, string $remark): bool
    {
        //处理数据
        $tempArr['CMD'] = $Cmd;
        $tempArr['IMEI'] = $Imei;
        $tempArr['REQ'] = uuid();
        $tempArr = array_merge($tempArr, $data);
        //计算签名
        $tempArr['CK'] = self::checkBit($tempArr);
        //转为json字符串
        $tempStr = json_encode($tempArr);
        //下发指令
        $obj = new EmqxHttpApi();
        $bool = $obj->publish($Imei, $tempStr, 2);
        $result = $bool['code'] == 0 ? '成功' : '失败';
        DeviceLog::writeDeviceLog($Cmd, $Imei, $tempArr['REQ'], $tempStr, '下行', $result, time(), $remark);
        return true;
    }

    /**
     * 计算校验位
     * @param array $data
     * @return string
     */
    private static function checkBit(array $data): string
    {
        $array = ['CK' => 'wusudiqiu'];
        return substr(md5(json_encode(array_merge($data, $array))), 1, 2);
    }

    /**
     * CRC校验
     * @param string $string
     * @return string
     */
    private static function crc16(string $string): string
    {
        $auchCRCHi = [

        ];
        $auchCRCLo = [

        ];
        $length = strlen($string);
        $ucCRCHi = 0xFF;
        $ucCRCLo = 0xFF;
        for ($i = 0; $i < $length; $i++) {
            $usIndex = $ucCRCHi ^ ord(substr($string, $i, 1));
            $ucCRCHi = $ucCRCLo ^ $auchCRCHi[$usIndex];
            $ucCRCLo = $auchCRCLo[$usIndex];
        }
        //转成16进制
        $CK = sprintf("%04s", dechex($ucCRCHi << 8 | $ucCRCLo));
        return strtoupper($CK);
    }

}
